Path: blob/master/src/packages/next/pages/news/[id].tsx
6095 views
/*1* This file is part of CoCalc: Copyright © 2023 Sagemath, Inc.2* License: MS-RSL – see LICENSE.md for details3*/45import { Alert, Breadcrumb, Col, Layout, Radio, Row } from "antd";6import { GetServerSidePropsContext } from "next";7import { useRouter } from "next/router";8import NextHead from "next/head";9import dayjs from "dayjs";1011import { getNewsItemUserPrevNext } from "@cocalc/database/postgres/news";12import getCustomize from "@cocalc/database/settings/customize";13import { Icon } from "@cocalc/frontend/components/icon";14import { markdown_to_cheerio } from "@cocalc/frontend/markdown";15import { slugURL } from "@cocalc/util/news";16import { NewsPrevNext } from "@cocalc/util/types/news";1718import Footer from "components/landing/footer";19import Head from "components/landing/head";20import Header from "components/landing/header";21import A from "components/misc/A";22import { News } from "components/news/news";23import { NewsWithStatus } from "components/news/types";24import { useDateStr } from "components/news/useDateStr";25import Loading from "components/share/loading";26import { MAX_WIDTH, NOT_FOUND } from "lib/config";27import { Customize, CustomizeType } from "lib/customize";28import useProfile from "lib/hooks/profile";29import { extractID } from "lib/news";30import withCustomize from "lib/with-customize";3132interface Props {33customize: CustomizeType;34news: NewsWithStatus;35prev?: NewsPrevNext;36next?: NewsPrevNext;37metadata: {38title: string;39author: string;40url: string;41image: string;42published: string;43modified: string;44};45}4647const formatNewsTime = (newsDate: NewsWithStatus["date"]) =>48(typeof newsDate === "number"49? dayjs.unix(newsDate)50: dayjs(newsDate)51).toISOString();5253export default function NewsPage(props: Props) {54const { customize, news, prev, next, metadata } = props;55const { siteName } = customize;56const router = useRouter();57const profile = useProfile({ noCache: true });58const isAdmin = profile?.is_admin;59const dateStr = useDateStr(news);60const permalink = slugURL(news);6162const title = `${news.title} – News – ${siteName}`;6364function future() {65if (news.future && !isAdmin) {66return (67<Alert type="info" banner={true} message="News not yet published" />68);69}70}7172function content() {73if (profile == null) return <Loading />;74if (!isAdmin && news.hide) {75return <Alert type="error" message="Not authorized" />;76}77if (isAdmin || !news.future) {78return <News news={news} showEdit={isAdmin} standalone />;79}80}8182function breadcrumb() {83const items = [84{ key: "/", title: <A href="/">{siteName}</A> },85{ key: "/news", title: <A href="/news">News</A> },86{87key: "permalink",88title: (89<A href={permalink}>90{isAdmin || (!news.future && !news.hide) ? (91<>92{dateStr}: {news.title}93</>94) : (95"Not Authorized"96)}97</A>98),99},100];101return <Breadcrumb items={items} />;102}103104function olderNewer() {105return (106<Radio.Group buttonStyle="outline" size="small">107<Radio.Button108disabled={!prev}109style={{ userSelect: "none" }}110onClick={() => {111prev && router.push(slugURL(prev));112}}113>114<Icon name="arrow-left" /> Older115</Radio.Button>116<Radio.Button117style={{ userSelect: "none" }}118onClick={() => {119router.push("/news");120}}121>122<Icon name="arrow-up" /> Overview123</Radio.Button>124<Radio.Button125disabled={!next}126style={{ userSelect: "none" }}127onClick={() => {128next && router.push(slugURL(next));129}}130>131<Icon name="arrow-right" /> Newer132</Radio.Button>133</Radio.Group>134);135}136137function renderTop() {138return (139<Row justify="space-between" gutter={15} style={{ margin: "30px 0" }}>140<Col>{breadcrumb()}</Col>141<Col>{olderNewer()}</Col>142</Row>143);144}145146return (147<Customize value={customize}>148<Head title={title} />149<NextHead>150<meta property="og:type" content="article" />151152<meta property="og:title" content={metadata.title} />153<meta property="og:url" content={metadata.url} />154<meta property="og:image" content={metadata.image} />155156<meta property="article:published_time" content={metadata.published} />157<meta property="article:modified_time" content={metadata.modified} />158</NextHead>159<Layout>160<Header />161<Layout.Content162style={{163backgroundColor: "white",164}}165>166<div167style={{168minHeight: "75vh",169maxWidth: MAX_WIDTH,170padding: "30px 15px",171margin: "0 auto",172}}173>174{renderTop()}175{future()}176{content()}177</div>178<Footer />179</Layout.Content>180</Layout>181</Customize>182);183}184185export async function getServerSideProps(context: GetServerSidePropsContext) {186const { query } = context;187const id = extractID(query.id);188if (id == null) return NOT_FOUND;189190try {191const { news, prev, next } = await getNewsItemUserPrevNext(id);192const { siteName, siteURL } = await getCustomize();193194if (news == null) {195throw new Error(`not found`);196}197198// Extract image URL from parsed Markdown. By converting to HTML first, we199// automatically add support for HTML that's been embedded into Markdown.200//201const $markdown = markdown_to_cheerio(news.text);202const imgSrc = $markdown("img").first().attr("src");203204// Format published time205//206const publishedTime = formatNewsTime(news.date);207208// Get the last-modified time by sorting the post history by timestamp,209// reversing it, and parsing the first element in that array.210//211const newsModificationTimestamps = Object.keys(news.history || {})212.map(Number)213.filter((ts) => !Number.isNaN(ts))214.sort()215.reverse();216217const modifiedTime = newsModificationTimestamps.length218? formatNewsTime(newsModificationTimestamps[0])219: publishedTime;220221const metadata: Props["metadata"] = {222title: news.title,223url: `${siteURL}${slugURL(news)}`,224image: imgSrc || "",225published: publishedTime,226modified: modifiedTime,227author: `${siteName}`,228};229230return await withCustomize({231context,232props: {233news,234prev,235next,236metadata,237},238});239} catch (err) {240console.warn(`Error getting news with id=${id}`, err);241}242243return NOT_FOUND;244}245246247